// Copyright (C) 2022 即时通讯网(52im.net) & Jack Jiang.
// The RainbowChat Project. All rights reserved.
//
// 【本产品为著作权产品，合法授权后请放心使用，禁止外传！】
// 【本次授权给：<广西木子科技有限公司>，授权编号：<NT220402151538>，代码指纹：<A.648883738.885>，技术对接人微信：<ID: Lingmuziyi>】
// 【授权寄送：<收件：李先生、地址：南宁市科园西十路11号国电智能大厦1101F、电话：17736659550、邮箱：yingshashou@vip.qq.com>】
//
// 【本系列产品在国家版权局的著作权登记信息如下】：
// 1）国家版权局登记名(简称)和权证号：RainbowChat    （证书号：软著登字第1220494号、登记号：2016SR041877）
// 2）国家版权局登记名(简称)和权证号：RainbowChat-Web（证书号：软著登字第3743440号、登记号：2019SR0322683）
// 3）国家版权局登记名(简称)和权证号：RainbowAV      （证书号：软著登字第2262004号、登记号：2017SR676720）
// 4）国家版权局登记名(简称)和权证号：MobileIMSDK-Web（证书号：软著登字第2262073号、登记号：2017SR676789）
// 5）国家版权局登记名(简称)和权证号：MobileIMSDK    （证书号：软著登字第1220581号、登记号：2016SR041964）
// * 著作权所有人：江顺/苏州网际时代信息科技有限公司
//
// 【违法或违规使用投诉和举报方式】：
// 联系邮件：jack.jiang@52im.net
// 联系微信：hellojackjiang
// 联系QQ号：413980957
// 授权说明：http://www.52im.net/thread-1115-1-1.html
// 官方社区：http://www.52im.net
package com.x52im.rainbowchat.http.file.u4web;

import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;

import com.eva.epc.common.file.FileHelper;
import com.eva.epc.common.util.CommonUtils;
import com.eva.framework.utils.LoggerFactory;

/**
 * 处理Http的文件上传实用类（目前用于Web端上传，，APP端暂不使用，因为Web客户端没有MD5码生成能力
 * ，而APP端是有的，所以Web端文件上传的服务端代码跟APP端是有少许区别的，暂时就不考虑重用了。）.
 * <p>
 * 注意：目前的RainbowChat-Web端（上传照片、文件）、APP端（上传照片）都是只允许一次传一个文件，
 * 不管是服务端还是客户端都是按1个文件的处理逻辑去实现。但因Apache的FileUpload库默认支持
 * 多个文件上传，且代码也只能按支持多个文件上传去写，所以本类中业务上是以一个文件的上传去
 * 实现（虽然代码中可以循环处理上传的多个文件）：即自动计算文件的MD5码（并返回）都是返回
 * 的最后一个文件的（ 技术上即使多个文件上传上来也只能返回最后一个文件的MD5码文件名）。
 * <p>
 * 基于未来可能的复杂业务的需要，本类是由com.eva.epc.tools.ends.ud.FileUpload代码扩展而来，不考虑重用。
 *
 * @author Jack Jiang
 * @version 1.0
 */
class FileUploadImpl {
//	private final static String TAG = FileUploadImpl.class.getSimpleName();

    /**
     * 文件上传处理实现方法。
     *
     * @param request
     * @param response
     * @param saveToDir         文件将要保存到的目录
     * @param THRESHOLD_SIZE    磁盘临时缓存文件阀值。
     *                          <pre>
     *                                  Apache文件上传组件在解析上传数据中的每个字段内容时，需要临时保存解析出的数据，以
     *                              便在后面进行数据的进一步处理（保存在磁盘特定位置或插入数据库）。因为Java虚拟机默
     *                              认可以使用的内存空间是有限的，超出限制时将会抛出“java.lang.OutOfMemoryError”错
     *                              误。如果上传的文件很大，例如800M的文件，在内存中将无法临时保存该文件内容，Apache
     *                              文件上传组件转而采用临时文件来保存这些数据；但如果上传的文件很小，例如600个字节的
     *                              文件，显然将其直接保存在内存中性能会更加好些。
     *
     *                                  setSizeThreshold方法用于设置是否将上传文件已临时文件的形式保存在磁盘的临界值（以
     *                              字节为单位的int值），如果从没有调用该方法设置此临界值，将会采用系统默认值10KB。对
     *                              应的getSizeThreshold() 方法用来获取此临界值。
     *                          </pre>
     * @param MAX_FILE_SIZE     单个文件最大大小
     * @param MAX_REQUEST_SIZE  所有文件总大小（因为本方法支持多文件上传）
     * @param neadRenameWithMD5 true表示需要把文件以它的MD5码形式进行重命名并存储（用于Web端上传时
     *                          ），否则原名不动（用于iOS APP端上传时）
     * @param action            上传类务类型，详见 {@link FileUploader4Web#ACTION_IMAGE_OF_CHAT_MSG_UPLOAD}、
     *                          {@link FileUploader4Web#ACTION_FILE_OF_CHAT_MSG_UPLOAD}
     * @return 文件上传及处理成功则返回新文件名（以md5码命名的）， 为null表示重命名失败
     * @throws Exception
     */
    public static HashMap<String, String> processFileUpload(HttpServletRequest request
            , HttpServletResponse response, String saveToDir
            , int THRESHOLD_SIZE, int MAX_FILE_SIZE, int MAX_REQUEST_SIZE
            , boolean neadRenameWithMD5
            , String action) throws Exception {
        // 此行代码解决网页端上传的中文文件名后，服务端读取到的是乱码的问题
        request.setCharacterEncoding("UTF-8");
        // 此行代码解决返回给web浏览器端的数据包括中文时，web端读取乱码的问题
        response.setCharacterEncoding("UTF-8");

        // 本方法调用完成时，将要返回的数据，用MAP是为了方便返回多个数据
        HashMap<String, String> returnMap = new HashMap<String, String>();

        // 上传下来的原始文件名
        String fileName = null;
        // 用md5码重命名后的存放于服务端的文件名
        String finalMD5FileName = null;
        // 上传的文件的总大小（单位：字节）
        long fileLength = 0;

        if (saveToDir == null)
            throw new Exception("[HTTP-" + FileUploader4Web.getLogTAG(action) + "] saveToDir == null!");

        // checks if the request actually contains upload file
        if (!ServletFileUpload.isMultipartContent(request)) {
            LoggerFactory.getLog().error("[HTTP-" + FileUploader4Web.getLogTAG(action) + "] 没有需要上传的文件，ServletFileUpload.isMultipartContent(request)=true!");
            throw new IOException("[HTTP-" + FileUploader4Web.getLogTAG(action) + "] Not file to upload!");
        }

        // configures upload settings
        DiskFileItemFactory factory = new DiskFileItemFactory();
        factory.setSizeThreshold(THRESHOLD_SIZE);
        factory.setRepository(new File(System.getProperty("java.io.tmpdir")));

        ServletFileUpload upload = new ServletFileUpload(factory);
        upload.setFileSizeMax(MAX_FILE_SIZE);
        upload.setSizeMax(MAX_REQUEST_SIZE);

        // constructs the directory path to store upload file
        String uploadPath = saveToDir;//getServletContext().getRealPath("") + File.separator + UPLOAD_DIRECTORY;
        // creates the directory if it does not exist
        File uploadDir = new File(uploadPath);
        if (!uploadDir.exists())
            uploadDir.mkdirs();

        try {
            // parses the request's content to extract file data
            List formItems = upload.parseRequest(request);
            Iterator iter = formItems.iterator();

            // iterates over form's fields
            while (iter.hasNext()) {
                FileItem item = (FileItem) iter.next();
                // processes only fields that are not form fields
                if (!item.isFormField()) {
                    fileName = new File(item.getName()).getName();

                    // 临时存储时的文件名加一个随机数，防止理论上可能出现的重命文件上传而出现同步修改问题
                    String tempFilePath = uploadPath + File.separator
                            + ((neadRenameWithMD5 ? (CommonUtils.getRandomStr2(5) + "_") : "") + fileName);

                    // 将上传的文件存储到临时位置
                    File storeFile = new File(tempFilePath);
                    // saves the file on disk
                    item.write(storeFile);

                    // 文件大小
                    fileLength = storeFile.length();

                    // 生成的新文件名是否需要带扩展名
                    boolean needExtName = true;
                    if (FileUploader4Web.isFileOfChatMsg(action) || FileUploader4Web.isFileOfAvatar(action)) {
                        needExtName = false;
                    }

                    // 最终存储的文件将以文件的MD5码重命名该文件，防止文件名相同但内容不同的情况发生
                    finalMD5FileName = neadRenameWithMD5 ?
                            renameUseMD5(storeFile, FileUploader4Web.getLogTAG(action), needExtName) : fileName;

                    // 本字段不为空即意味着以MD5码重命名成功（如果需要以MD5码命名的话）
                    if (finalMD5FileName != null) {
//						// 如果是图片聊天消息中的图片，则还需要生成缩略图（以便APP或Web端方便预览）
//						if(FileUploader4Web.isImageOfChatMsg(action))
//						{
//							// 生成原图的缩略图并存到磁盘
//							File thumbnailFilePath = new File(uploadPath + File.separator + "th_" + finalMD5FileName);
//							// 如果该md5文件名命名（其实此md5码就是文件数据的指纹）存在，即意味着不需要写入磁盘了
//							if(!thumbnailFilePath.exists())
//							{
//								// 用等比缩放并裁切的算法（此算法自2014-03-29实施），算法将保证缩略图的生成不变形
//								try
//								{
//									ImgEqualScaleAndCutHelper.saveImageAsJpg(
//											uploadPath + File.separator + finalMD5FileName
//											, thumbnailFilePath.getAbsolutePath()
//											, 200, 200 // 生成的缩略图参考微信的按100*100的极限缩放
//											);
//								}
//								catch (Exception e)
//								{
//									LoggerFactory.getLog().warn("[HTTP-"+FileUploader4Web.getLogTAG(action)+"] 图片缩略图生成时出错了！", e);
//								}
//							}
//							else
//							{
//								LoggerFactory.getLog().warn("[HTTP-"+FileUploader4Web.getLogTAG(action)+"] 图片缩略图"
//										+thumbnailFilePath.getAbsolutePath()+"已经存在，不需要再生成了哦！");
//							}
//						}
                    } else {
                        LoggerFactory.getLog().error("[HTTP-" + FileUploader4Web.getLogTAG(action) + "] finalMD5FileName==null(" +
                                "neadRenameWithMD5?" + neadRenameWithMD5 + ")，本次处理没有继续！");
                    }
                }
            }
//			request.setAttribute("message", "Upload has been done successfully!");
//			sucess = true;
        } catch (Exception ex) {
            LoggerFactory.getLog().error("[HTTP-" + FileUploader4Web.getLogTAG(action) + "] 处理文件上传时出错了，" + ex.getMessage(), ex.getCause());
            throw ex;
//			request.setAttribute("message", "There was an error: " + ex.getMessage());
        }

//		getServletContext().getRequestDispatcher("/message.jsp").forward(request, response);
//		return sucess;

        // 图片消息文件上传时
        if (FileUploader4Web.isImageOfChatMsg(action)) {
            // 以下参数将作为JSON对象的一部分回传给web端
            returnMap.put("fileNameMD5", finalMD5FileName);
        }
        // 头像的图文件上传时
        else if (FileUploader4Web.isFileOfAvatar(action)) {
            returnMap.put("fileNameMD5", finalMD5FileName);
        }
        // 普通文件上传时
        else {
            // 以下参数将作为JSON对象的一部分回传给web端
            returnMap.put("fileName", fileName);
            returnMap.put("fileMd5", finalMD5FileName);
            returnMap.put("fileLength", "" + fileLength);
        }

        return returnMap;
    }

    /**
     * 以文件的MD5码形式重命名该文件。
     *
     * @param tempFileSavedPath
     * @param needExtName       true表示重命名后的新文件名需要带上原扩展名，否则不需要扩展名
     * @return 重命名成功则返回新文件名， 为null表示重命名失败
     */
    private static String renameUseMD5(File tempFileSavedPath, String logTag, boolean needExtName) {
        String finalFileName = null;

        // 计算出文件的MD5码
        byte[] fileData = null;
        try {
            // 将要上传的图像文件数据读取出来
            fileData = FileHelper.readFileWithBytes(tempFileSavedPath);

            // 读取出来的数据是空的，上传当然就没有必要继续了
            if (fileData == null)
                return null;

            // 计算图片文件的MD5码
            final String fileMd5 = getTempFileMD5(fileData, logTag);
            if (fileMd5 != null) {
                // 以MD5码形式命名的新文件名
                String fileNameUsedMd5 = constructNewFileName(fileMd5
                        , tempFileSavedPath.getName(), logTag, needExtName);

                File fileAfterRename = null;
                try {
                    //
                    fileAfterRename = new File(tempFileSavedPath.getParent() + "/" + fileNameUsedMd5);

                    // 如果这个文件存在，就不用再处理了，直接返回
                    if (!fileAfterRename.exists()) {
                        tempFileSavedPath.renameTo(fileAfterRename);
                    } else {
                        LoggerFactory.getLog().info("[HTTP-" + logTag + "] " + fileAfterRename.getAbsolutePath()
                                + "已经存在，不需要理费劲重新生成MD5命名的文件了。");

                        // 尝试把上传时的临时文件删除（未用MD5码重命名前的文件）
                        try {
                            tempFileSavedPath.delete();
                        } catch (Exception e) {
                            LoggerFactory.getLog().warn(e.getMessage(), e);
                        }
                    }
//					processedFileName = fileAfterRename.getName();

                    finalFileName = fileNameUsedMd5;
                } catch (Exception e) {
                    LoggerFactory.getLog().error("[HTTP-" + logTag + "] 将临时文件" + tempFileSavedPath + "重命名失败了，上传将不能继续！", e);
                    // 如果重命名失败，就尝试把这个文件删除（没有必要留了）
                    try {
                        fileAfterRename.delete();
                    } catch (Exception e2) {
                    }
                }
            }
        }
        // 显示处理下OOM使得APP更健壮（OOM只能显示抓取，否则会按系统Error的方式报出从而使APP崩溃哦）
        catch (OutOfMemoryError e) {
            LoggerFactory.getLog().error("[HTTP-" + logTag + "] 将图片文件数据读取到内存时内存溢出了，上传没有继续！", e);
        } catch (Exception e) {
            LoggerFactory.getLog().error("[HTTP-" + logTag + "] 尝试将图片临时文件数据读取出来时出错了，"
                    + e.getMessage() + "，上传将不能继续！", e);
        }

        return finalFileName;
    }

    private static String getTempFileMD5(byte[] fileData, String logTag) {
        try {
            return FileHelper.getFileMD5(fileData);
        } catch (Exception e) {
            LoggerFactory.getLog().error("[HTTP-" + logTag + "] 计算MD5码时出错了，" + e.getMessage(), e);
            return null;
        }
    }

    /**
     * 返回以MD5码的形式的新文件名.
     *
     * @param uid
     * @param md5ForCachedAvatar
     * @return
     */
    public static String constructNewFileName(String md5ForCachedAvatar
            , String originalFileName, String logTag, boolean needExtName) {
        if (md5ForCachedAvatar == null) {
            LoggerFactory.getLog().warn("[HTTP-" + logTag + "] 无效的参数：md5ForImage == null!");
            return null;
        }

        return md5ForCachedAvatar + (needExtName ? ("." + FileHelper.getFileExName(originalFileName)) : "");
    }


}
